package blazingbubble.game;

import moltencore.game.AbstractField;
import moltencore.game.CannonData;
import moltencore.game.collision.ICollidable;
import moltencore.game.loader.Level;
import moltencore.game.loader.LevelSprite;
import spark.math.Vector2;
import spark.math.Random;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.log4j.Logger;

public class Field extends AbstractField<Bubble, Player> {
	private static Logger logger = Logger.getLogger(Field.class);

	private Vector2 resultPosition;
	private Vector2 nickPosition;
	private Vector2 scorePosition;
	private Collection<LevelSprite> levelSprites;

	private final ReadWriteLock lock = new ReentrantReadWriteLock();
	private final Lock readLock = lock.readLock();
	private final Lock writeLock = lock.writeLock();

	private final HashMap<Short, Bubble> bubbles = new HashMap<Short, Bubble>();
	private Vector2 destroyingBubbleGravity = new Vector2(0, -36);
	private ArrayList<ICollidable> killAreas;

	/**
	 * @param fieldId        Unique id (within a game)
	 * @param fieldFile      File used to initialize this field (levels folder)
	 * @param windEnabled    Should the field have wind that influences bubbles?
	 * @param gravityEnabled Should the field have gravity that influences bubbles?
	 * @param collidables    Walls etc. all moving bubbles can collide against
	 * @param criticalAreas  Collidables that make a moving bubble into a penalty bubble if entered
	 * @param killAreas      Areas that once touched remove a bubble which is in the 'destroying' state
	 * @param cannonData     Information about cannon properties specified in the level file
	 * @param resultPosition Vector at which to display the game result at the end of the game
	 * @param nickPosition   Vector at which to display the nickname of the field's owner(s)
	 * @param scorePosition  Vector at which to display the score
	 * @param levelSprites   Sprites for this level, can be null
	 */
	public Field(long fieldId, String fieldFile,
	             boolean windEnabled, boolean gravityEnabled,
	             ArrayList<ICollidable> collidables,
	             ArrayList<ICollidable> criticalAreas,
	             ArrayList<ICollidable> killAreas,
	             Collection<CannonData> cannonData, Vector2 resultPosition,
	             Vector2 nickPosition, Vector2 scorePosition,
	             Collection<LevelSprite> levelSprites) {
		super(fieldId, fieldFile, windEnabled, gravityEnabled, collidables, criticalAreas, cannonData);
		this.levelSprites = levelSprites;
		this.resultPosition = resultPosition;
		this.nickPosition = nickPosition;
		this.scorePosition = scorePosition;
		this.killAreas = killAreas;
	}

	public void spawnBubble(Bubble bubble) {
		getWriteLock().lock();
		try {
			bubbles.put(bubble.getBubbleId(), bubble);
		}
		finally {
			getWriteLock().unlock();
		}
	}

	public void pushCeiling(Vector2 pushDirection) {
		getReadLock().lock();
		try {
			for(Bubble bubble : bubbles.values()) {
				if(bubble.getState() == Bubble.State.Fixed) {
					bubble.getWriteLock().lock();
					try {
						bubble.getPosition().add(pushDirection);
					}
					finally {
						bubble.getWriteLock().unlock();
					}
				}
			}
		}
		finally {
			getReadLock().unlock();
		}
	}

	@Override
	public void update(float elapsedSeconds) {
		LinkedList<Short> bubblesToDestroy = null;
		getReadLock().lock();
		try {
			for(Bubble bubble : bubbles.values()) {
				boolean destroyBubble = updateBubble(bubble, elapsedSeconds);
				if(destroyBubble) {
					if(bubblesToDestroy == null) {
						bubblesToDestroy = new LinkedList<Short>();
					}
					bubblesToDestroy.add(bubble.getBubbleId());
				}
			}
		}
		finally {
			getReadLock().unlock();
		}
		removeBubbles(bubblesToDestroy);
	}

	private void removeBubbles(LinkedList<Short> bubblesToDestroy) {
		if(bubblesToDestroy != null) {
			getWriteLock().lock();
			try {
				for(Short bubbleId : bubblesToDestroy) {
					bubbles.remove(bubbleId);
				}
			}
			finally {
				getWriteLock().unlock();
			}
		}
	}

	/**
	 * Updates a bubble
	 *
	 * @param bubble         Bubble to update
	 * @param elapsedSeconds Seconds this bubble has to move
	 * @return true if the bubble has to be destroyed, false otherwise
	 */
	private boolean updateBubble(Bubble bubble, float elapsedSeconds) {
		bubble.getWriteLock().lock();
		try {
			switch(bubble.getState()) {
				case Moving:
					if(!bubble.getVelocity().isZero()) {
						updateMovingBubble(bubble, elapsedSeconds);
					}
					return false;
				case Destroying:
					return updateDestroyingBubble(bubble, elapsedSeconds);
				default:
					return false;
			}
		}
		finally {
			bubble.getWriteLock().unlock();
		}
	}

	private void updateMovingBubble(Bubble bubble, float elapsedSeconds) {
		applyGravity(bubble, gravity, elapsedSeconds);
		Vector2 movement = calculateMovement(bubble, elapsedSeconds);
		boolean collisionOccurred = handleCollidablesCollision(bubble, movement, null);
		if(!collisionOccurred) {
			for(Bubble otherBubble : bubbles.values()) {
				// only collide the bubble against fixed bubbles
				if(otherBubble.getState() == Bubble.State.Fixed) {
					collisionOccurred = handleMovingBubbleCollisionLogic(otherBubble, bubble, movement, null);
					if(collisionOccurred) {
						break;
					}
				}
			}
		}
		if(!collisionOccurred) {
			for(ICollidable criticalArea : criticalAreas) {
				collisionOccurred = handleMovingBubbleCollisionLogic(criticalArea, bubble, movement, null);
				if(collisionOccurred) {
					break;
				}
			}
		}
		applyMovement(bubble, movement);
	}

	private boolean updateDestroyingBubble(Bubble bubble, float elapsedSeconds) {
		applyGravity(bubble, destroyingBubbleGravity, elapsedSeconds);
		applyMovement(bubble, elapsedSeconds);
		for(ICollidable killArea : killAreas) {
			if(killArea.collides(bubble, null)) {
				return true;
			}
		}
		return false;
	}

	private void applyMovement(Bubble bubble, float elapsedSeconds) {
		bubble.getPosition().x += bubble.getVelocity().x * elapsedSeconds;
		bubble.getPosition().y += bubble.getVelocity().y * elapsedSeconds;
	}

	public Collection<Bubble> getBubbles() {
		return bubbles.values();
	}

	public Bubble getBubble(short id) {
		getReadLock().lock();
		try {
			return bubbles.get(id);
		}
		finally {
			getReadLock().unlock();
		}
	}

	public Lock getReadLock() {
		return readLock;
	}

	public Lock getWriteLock() {
		return writeLock;
	}

	public Vector2 getResultPosition() {
		return resultPosition;
	}

	public Vector2 getNickPosition() {
		return nickPosition;
	}

	public Vector2 getScorePosition() {
		return scorePosition;
	}

	@Override
	public void pop(Bubble bubble) {
		bubble.getWriteLock().lock();
		try {
			bubble.setState(Bubble.State.Destroying);
			initializePoppingVelocity(bubble);
		}
		finally {
			bubble.getWriteLock().unlock();
		}
	}

	private void initializePoppingVelocity(Bubble bubble) {
		int xVelocitySpread = 8;
		bubble.getVelocity().copy(new Vector2(
				Random.getInstance().nextInt(xVelocitySpread) - xVelocitySpread / 2,
				(4 + Random.getInstance().nextInt(3))
		));
	}

	public Collection<LevelSprite> getLevelSprites() {
		return levelSprites;
	}
}
